package com.hockeo.tinygds;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.google.appengine.api.datastore.DatastoreServiceConfig;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.ReadPolicy;
import com.google.apphosting.api.DatastorePb.DatastoreService;
import com.hockeo.tinygds.error.TNotLoadableException;
import com.hockeo.tinygds.error.TinyGDSException;

/**
 * Factory for {@link TinyGDS} objects.
 *
 * @version $Id$
 * @author jjanke
 */
public class TinyGDSFactory
{
  private final Map<String, TMetaData<?>> d_mapMetadataByClass         = new ConcurrentHashMap<String, TMetaData<?>>();
  private final Map<String, TMetaData<?>> d_mapMetadataByDiscriminator = new ConcurrentHashMap<String, TMetaData<?>>();

  private ReadPolicy                      d_readPolicy                 = new ReadPolicy( ReadPolicy.Consistency.STRONG );
  private double                          d_dDeadline                  = 0;

  /**
   * Registers the classes that represent persistant entities.
   *
   * @param acls the classes of the persistent entities
   */
  @SuppressWarnings( "unchecked" )
  public void register( Class<?>... acls )
  {
    for ( Class<?> cls : acls )
    {
      TMetaData<?> metadata = new TMetaData( this, cls );
      d_mapMetadataByClass.put( cls.getName(), metadata );
      d_mapMetadataByDiscriminator.put( metadata.getDiscriminator(), metadata );
    }
  }

  /**
   * Constructs a new {@link TinyGDS} object which does not use a transaction and returns
   * it. This is a very lightweight method and can be called as many times as needed.
   */
  public TinyGDS createTinyGDS()
  {
    return new TinyGDS( DatastoreServiceFactory.getDatastoreService( getDatastoreServiceConfig() ), this );
  }

  /**
   * Constructs a new {@link TinyGDS} object which does not use a transaction and returns
   * it. This is a very lightweight method and can be called as many times as needed.
   *
   * @param cfg a datastore service configuration to be used
   */
  public TinyGDS createTinyGDS( DatastoreServiceConfig cfg )
  {
    return new TinyGDS( DatastoreServiceFactory.getDatastoreService( cfg ), this );
  }

  /**
   * Returns the read policy which is used when creating the next TinyGDS object using
   * {@link #createTinyGDS()}.
   */
  public ReadPolicy getReadPolicy()
  {
    return d_readPolicy;
  }

  /**
   * Sets the read policy to be used for the next creation of a TinyGDS object.
   *
   * @param readPolicy the readPolicy to set
   */
  public void setReadPolicy( ReadPolicy readPolicy )
  {
    d_readPolicy = readPolicy;
  }

  /**
   * Returns the deadline in seconds to be used as timeout for datastore API calls.
   *
   * @return the deadline in seconds, 0 to not have any deadline
   */
  public double getDeadline()
  {
    return d_dDeadline;
  }

  /**
   * Sets the deadline to be used as timeout for datastore API calls.
   *
   * @param dDeadline the deadline to set, 0 to disable deadline
   */
  public void setDeadline( double dDeadline )
  {
    d_dDeadline = dDeadline;
  }

  /**
   * Converts a type-safe {@link TKey} instance into a Google Datastore raw {@link Key}
   * representation.
   *
   * @param tKey the key to be converted
   * @return the converted key
   */
  public Key tKeyToRawKey( TKey<?> tKey )
  {
    // an object may not (yet) have a key, therefore allow null
    //
    if ( tKey == null )
      return null;

    if ( tKey.getName() != null )
      return KeyFactory.createKey( tKeyToRawKey( tKey.getParent() ), getMetaData( tKey.getEntityClass() ).getKind(),
          tKey.getName() );
    else
      return KeyFactory.createKey( tKeyToRawKey( tKey.getParent() ), getMetaData( tKey.getEntityClass() ).getKind(),
          tKey.getId() );
  }

  /**
   * Creates a {@link TKey} representation for the given target type which is indicated by
   * the 2nd argument. The key is build from the given raw key.
   *
   * @param <T> the type of the persistent entity class to which the key refers
   * @param keyRaw the raw key to use as the source for the new key
   * @param clsKeyType the target key's type argument
   * @return a key that matches the given raw key, may be <code>null</code>
   */
  @SuppressWarnings( "unchecked" )
  public <T> TKey<T> rawKeyToTKey( Key keyRaw, Class<T> clsKeyType )
  {
    if ( keyRaw == null )
      return null;

    // do we have a parent key?
    //
    Key keyRawParent = keyRaw.getParent();
    TKey<?> keyParent = null;
    if ( keyRawParent != null )
    {
      // to identify the right parent key type, we need the meta data
      //
      TMetaData<T> metadata = getMetaData( clsKeyType );
      Class<?> clsParentKeyType = metadata.getParentEntityClass();

      if ( clsParentKeyType == null )
        throw TNotLoadableException.noParentField( keyRaw, clsKeyType );

      if ( keyRawParent.getName() != null )
        keyParent = new TKey( clsParentKeyType, keyRawParent.getName() );
      else
        keyParent = new TKey( clsParentKeyType, keyRawParent.getId() );
    }

    // the actual main key
    //
    if ( keyRaw.getName() != null )
      return new TKey<T>( keyParent, clsKeyType, keyRaw.getName() );
    else
      return new TKey<T>( keyParent, clsKeyType, keyRaw.getId() );
  }

  /**
   * Returns a new uninitialised instance of the given class. No property of the instance
   * will be set, not even the primary key! Be careful when using this method.
   *
   * @param strClassName the fully qualified name of the class to be instantiated
   * @return a new uninitialised instance
   */
  public Object createNewUninitialisedInstance( String strClassName )
  {
    return getMetaDataByClassName( strClassName ).createNewInstance();
  }

  /**
   * Returns a new uninitialised instance of the given class. No property of the instance
   * will be set, not even the primary key! Be careful when using this method.
   *
   * @param <T> the type to instantiate
   * @param cls the class which should be instantiated
   * @return a new uninitialised instance
   */
  public <T> T createNewUninitialisedInstance( Class<T> cls )
  {
    return getMetaData( cls ).createNewInstance();
  }

  /**
   * Returns a raw key for the given object which can be either a Google Datastore Key, a
   * TKey or a persistent TinyGDS entity.
   *
   * @param objKeyOrEntity the object for which to retrieve the raw key
   * @return the matching raw key
   */
  Key getRawKey( Object objKeyOrEntity )
  {
    if ( objKeyOrEntity instanceof Key )
      return (Key) objKeyOrEntity;

    if ( objKeyOrEntity instanceof TKey<?> )
      return tKeyToRawKey( (TKey<?>) objKeyOrEntity );

    TMetaData<?> md = getMetaData( objKeyOrEntity.getClass() );
    if ( md == null )
      throw new IllegalArgumentException( "The given object '" + objKeyOrEntity.toString()
          + "' is neither a Key, a TKey or a registered persistent TinyGDS entity." );

    return md.getKey( objKeyOrEntity );
  }

  /**
   * Returns the {@link TMetaData} instance associated with the given key.
   *
   * @param <T> represents the entity type
   * @param key the key for which to retrieve the meta data
   * @return the matching TMetaData instance
   */
  <T> TMetaData<? extends T> getMetaData( final TKey<T> key )
  {
    return getMetaData( key.getEntityClass() );
  }

  /**
   * Returns the {@link TMetaData} instance associated with the class whose qualified name
   * is specified.
   *
   * @param <T> represents the entity type
   * @param cls the class for which to retrieve the meta data
   * @return the matching TMetaData instance or null if there is none
   */
  @SuppressWarnings( "unchecked" )
  <T> TMetaData<T> getMetaData( final Class<T> cls )
  {
    TMetaData<T> metadata = (TMetaData<T>) d_mapMetadataByClass.get( cls.getName() );

    if ( metadata == null )
    {
      metadata = new TMetaData<T>( this, cls );
      d_mapMetadataByClass.put( cls.getName(), metadata );
    }

    return metadata;
  }

  /**
   * Convenience method that retrieves the TMetadata for the persistant class matching the
   * given datastore entity. The matching TMetaData object is determined by examining the
   * entity's discriminator property.
   *
   * @param ent the entity for which to return the matching TMetaData object
   * @return the matching TMetaData object
   * @throws TNotLoadableException if the specified entity has no
   *           <code>__discriminator__</code> property
   * @throws TinyGDSException if no matching metadata is available
   */
  TMetaData<?> getMetaData( Entity ent )
  {
    String strDiscriminator = (String) ent.getProperty( TMetaData.DISCRIMINATOR_PROPERTY );

    if ( strDiscriminator == null )
      throw TNotLoadableException.missingDiscriminator( ent );

    return getMetaDataByDescriminator( strDiscriminator );
  }

  /**
   * Returns the TMetaData object matching the class with the given discriminator.
   *
   * @param strDiscriminator the discriminator of the class to be retrieved
   * @return the matching TMetaData object
   * @throws TinyGDSException if no matching metadata is available
   */
  TMetaData<?> getMetaDataByDescriminator( String strDiscriminator )
  {
    TMetaData<?> metadata = d_mapMetadataByDiscriminator.get( strDiscriminator );

    if ( metadata == null )
      throw new TinyGDSException( "No metadata is registered for the discriminator " + strDiscriminator );

    return metadata;
  }

  /**
   * Returns the TMetaData object matching the class with the given name.
   *
   * @param strClassName the fully qualified name of the class for which to retrieve
   *          metadata
   * @return the matching TMetaData object
   * @throws TinyGDSException if no matching metadata is available
   */
  TMetaData<?> getMetaDataByClassName( String strClassName )
  {
    TMetaData<?> metadata = d_mapMetadataByClass.get( strClassName );

    if ( metadata == null )
    {
      try
      {
        metadata = getMetaData( Class.forName( strClassName ) );
      }
      catch ( ClassNotFoundException ex )
      {
        throw new TinyGDSException( "The '" + strClassName + "' was not found.", ex );
      }
    }

    return metadata;
  }

  /**
   * Returns all known TMetaData objects that are associated to the same kind. May not
   * return all metadata objects if not all persistent classes have been registered (yet).
   *
   * @param strKind the kind for which all known metadata objects should be returned
   * @return all known metadata objects having the same kind
   */
  List<TMetaData<?>> getMetaDataByKind( final String strKind )
  {
    List<TMetaData<?>> list = new ArrayList<TMetaData<?>>();

    for ( TMetaData<?> metadata : d_mapMetadataByClass.values() )
      if ( metadata.getKind().equals( strKind ) )
        list.add( metadata );

    return list;
  }

  /**
   * Returns all known TMetaData objects that are referencing subclasses of the class
   * represented by the parent TMetaData object.
   *
   * @param <T> the class of the parent TMetaData object
   * @param parent the parent object
   * @return a list that contains the given parent and all known TMetaData objects that
   *         describe sub classes of the given parent
   */
  @SuppressWarnings( "unchecked" )
  <T> List<TMetaData<? extends T>> getMetaDataByHierarchy( final TMetaData<T> parent )
  {
    List<TMetaData<? extends T>> list = new ArrayList<TMetaData<? extends T>>();

    // add the parent
    //
    list.add( parent );

    // find the children
    //
    for ( TMetaData<?> metadata : getMetaDataByKind( parent.getKind() ) )
      if ( parent.getEntityClass().isAssignableFrom( metadata.getEntityClass() ) )
        list.add( (TMetaData<? extends T>) metadata );

    return list;
  }

  /**
   * Creates the datastore configuration to be used when instantiating a
   * {@link DatastoreService}.
   *
   * @return a new DatastoreServiceConfig
   */
  private DatastoreServiceConfig getDatastoreServiceConfig()
  {
    DatastoreServiceConfig dsc = DatastoreServiceConfig.Builder.withReadPolicy( d_readPolicy );

    if ( d_dDeadline > 0 )
      dsc.deadline( d_dDeadline );

    return dsc;
  }
}
